/**
* Copyright (c) 2010, Attila Magyar
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the Attila Magyar nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

package org.uftp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Vector;
import org.uftp.buffers.BufferFactory;
import org.uftp.buffers.ReadBuffer;
import org.uftp.log.FTPLogManager;
import org.uftp.log.FTPLogger;
import org.uftp.net.channels.ControlChannel;
import org.uftp.net.channels.DataChannel;
import org.uftp.monitors.FTPMessageListener;
import org.uftp.monitors.ProgressListener;
import org.uftp.net.SocketManagerFactory;

public class FTPBase {
        
    protected static FTPLogger logger = FTPLogManager.getDefaultLogger( "org.uftp" );
        
    /**
     * <b>Platform specific</b> line separator.
     */
    protected byte[] LINE_SEP;
    
    protected final static byte CR = 13;
    protected final static byte LF = 10;
    
    protected int chunkSize = 65535;
    protected byte[] buffer;
    protected ControlChannel control;
    protected int mode = FTPMode.PASSIVE;
    protected String accountInfo;
    protected String transferType = FTPType.IMAGE;
    protected Vector progressListeners;
    
    
    public FTPBase() {
        this( "", 21 );
    }
    
    public FTPBase( String remoteHost ) {
        this( remoteHost, 21 );
    }
    
    public FTPBase(String remoteHost, int remotePort) {

        if ( System.getProperty("line.separator") != null ) {
            LINE_SEP = System.getProperty("line.separator").getBytes();
        } else {
            LINE_SEP = "\n".getBytes();
        }

        if ( SocketManagerFactory.getDefaultManager() == null ) {
            SocketManagerFactory.createDefaultManager();
        }
        buffer = new byte[ chunkSize ];
        progressListeners = new Vector();
        
        control = new ControlChannel( remoteHost, remotePort );
    }
    
    public void login(String user, String pass) throws FTPSocketException, AuthenticationException {
        FTPReply rp = new FTPReply();
        
        try {
            rp = control.sendCommand( "USER " + user );
            rp.validate( new int[]{ 230, 331 } );
            
            // 230 User logged in, proceed.
            // 331 User name okay, need password.
            // 202 Command not implemented, superfluous at this site.
            // 332 Need account for login.
            
            if ( rp.getCode() == 331 ) { // password required
                
                rp = control.sendCommand( "PASS " + pass );
                rp.validate( new int[]{ 230, 332 } );
                
                if ( rp.getCode() == 332 ) { // account required
                    
                    rp = control.sendCommand("ACCT " + accountInfo);
                    rp.validate( 230 );
                }
            }
            
        } catch ( InvalidReplyException e ) {
            throw new AuthenticationException( "Authentication failed for user: " + user + "; " + rp );
        }
    }
    
    public void connect() throws FTPSocketException, InvalidReplyException {
        control.connect();
        // 220 Service ready for new user.
        control.readReply().validate( 220 ); // validate connection
    }
    
    public void disconnect() {
        try {
            control.sendCommand( "QUIT" );
        } catch ( InvalidReplyException e) {
            logger.exception( e, "Quit: invalid reply" );
        } catch ( FTPSocketException e ) {
            logger.exception( e, "Quit: socket exception" );
        } finally {
            try {
                control.close();
            } catch (FTPSocketException e) {
                logger.exception( e, "Closing: socket exception" );
            }
            
            
        }
    }
        
    public void cdup() throws FTPSocketException, InvalidReplyException {
//      200 Command okay.
//               CDUP
//                  200
//                  500, 501, 502, 421, 530, 550
        
        control.sendAndValidateCommand( "CDUP", new int[]{ 200, 250} );
        
    }
    
    public void cwd( String dir ) throws FTPSocketException, InvalidReplyException {
//      250 Requested file action okay, completed.
//      550 Requested action not taken. File unavailable (e.g., file not found, no access).
//               CWD
//                  250
//                  500, 501, 502, 421, 530, 550
        control.sendAndValidateCommand( "CWD " + dir, 250 );
    }
    
    
    public boolean delete( String file ) throws FTPSocketException, InvalidReplyException {
//               DELE
//                  250
//                  450,
//                  500, 501, 502, 421, 530, 550
        
        FTPReply rp =
                control.sendAndValidateCommand( "DELE " + file, new int[]{ 200, 250, 550 } );
        
        // not exists or no access to delete
        return !rp.checkCode( 550 );
    }
    
    public boolean rmdir( String dir ) throws FTPSocketException, InvalidReplyException {
//               RMD
//                  250
//                  500, 501, 502, 421, 530, 550
        
        FTPReply rp =
                control.sendAndValidateCommand( "RMD " + dir, new int[]{200, 250, 257, 550} );
        
        // already exists or no access
        return !rp.checkCode( 550 );
    }
    
    public boolean mkdir( String dir ) throws FTPSocketException, InvalidReplyException {
//      257 "PATHNAME" created.
//               MKD
//                  257
//                  500, 501, 502, 421, 530, 550
        FTPReply rp =
                control.sendAndValidateCommand( "MKD " + dir, new int[]{200, 250, 257, 550, 521 } );
        
        // already exists or no access
        return !rp.checkCode( new int[]{ 550, 521 } );
    }
    
    public boolean forceMkdir( String[] dirHierarchy ) throws FTPSocketException, InvalidReplyException {
        String acwd = pwd();
        try {
            for ( int i = 0; i < dirHierarchy.length; i++ ) {
                mkdir( dirHierarchy[i] );
                cwd( dirHierarchy[i] );
            }
        } finally {
            cwd( acwd );
        }
        return true;
    }
    
    public void nop() throws FTPSocketException, InvalidReplyException {
//               NOOP
//                  200
//                  500 421
        control.sendAndValidateCommand( "NOOP", 200 );
    }
    
    public long size( String file ) throws FTPSocketException, InvalidReplyException {
        FTPReply rp = control.sendCommand( "SIZE " + file );
        rp.validate( 213 );
        
        String s = rp.getText();
        int p = s.indexOf(' ');
        if ( p >= 0 ) {
            s = s.substring(0, p);
        }
        
        try {
            return Long.parseLong( s );
        } catch (NumberFormatException ex) {
            throw new InvalidReplyException("Failed to parse size: " + rp.toString() );
        }
    }
    
    public String pwd() throws FTPSocketException, InvalidReplyException {
//               PWD
//                  257
//                  500, 501, 502, 421, 550
        FTPReply rp = control.sendCommand( "PWD" );
        rp.validate( 257 );
        
        String msg = rp.getText();
        
        int s = msg.indexOf('"');
        int e = msg.lastIndexOf('"');
        
        if (s >= 0 && e > s) {
            String d = msg.substring(s +1, e);
            if ( d.endsWith("/") && d.length() > 1 ) {
                return d.substring( 0, d.length() -1 );
            }
            return d;
        } else {
            return msg;
        }
    }
    
    public void type( String type ) throws FTPSocketException, InvalidReplyException {
        control.sendAndValidateCommand( "TYPE " + type, 200 );
        this.setTransferType(type);
    }
    
    private void uploadData( InputStream is, OutputStream os ) throws IOException {
        
        boolean isAscii = FTPType.ASCII.equals( transferType );
        
        OutputStream out = BufferFactory.newOutputStream( os );
        InputStream in = BufferFactory.newInputStream( is );
        
        long transfered = 0;
        int count = in.read( buffer );
        
        if ( chunkSize < LINE_SEP.length ) {
            throw new IllegalArgumentException( "chunkSize < LINE_SEP.length" );
        }
        
        int j = 0;
        int startPos;
        
        while ( count > 0 ) {
            
            if ( isAscii ) {
                
                startPos = 0;
                for ( int i = 0; i < count; i++ ) {
                    
                    int ij = j;
                    int b = 0;
                    while ( i +b < count && j < LINE_SEP.length && buffer[ i +b ] == LINE_SEP[ j ]  ) {
                        j++; b++;
                    }
                    
                    if ( j == LINE_SEP.length ) {
                        // found CRLF
                        j = 0;
                        if ( i - startPos > 0 ) {
                            out.write( buffer, startPos, i - startPos );
                            out.flush();
                            transfered += i - startPos;
                        }
                        out.write( CR );
                        out.write( LF );
                        out.flush();
                        transfered += 2;
                        startPos = i + LINE_SEP.length - ij;
                        i = startPos -1; // for will increment +1
                    } else if ( i == 0 && ij != 0 ) {
                        out.write( LINE_SEP, 0, j ); // data from the previous chunk
                        transfered += j;
                    }
                    if ( i < count -1) { // not end of chunk
                        j = 0;
                    }
                }
                
                if ( startPos < count ) {
                    if (  count - startPos - j > 0 ) {
                        out.write( buffer, startPos, count - startPos - j);
                        transfered += count - startPos -j;
                        out.flush();
                    }
                }
                
                notifyProgressListener( transfered );
                
            } else {
                out.write( buffer, 0, count );
                out.flush();
                transfered += count;
                notifyProgressListener( transfered  );
            }
            
            count = in.read( buffer );
        }
        
    }
    
    public void upload( InputStream in, String remoteFile ) throws InvalidReplyException, FTPSocketException {
//               STOR
//                  125, 150
//                     (110)
//                     226, 250
//                     425, 426, 451, 551, 552
//                  532, 450, 452, 553
//                  500, 501, 421, 530
        
        
        DataChannel dch = null;
        OutputStream out = null;
        type( this.getTransferType() );
        
        boolean transfered = false;
        try {
            
            dch = control.createDataConnection( this.mode );
                        
            control.sendAndValidateCommand( "STOR " + remoteFile, new int[]{ 125, 150, 350 } );
            
            out = dch.getOutputStream();
            
            logger.debug( "Uploading..." );
            uploadData( in, out );
            logger.debug( "Upload: done." );
            
            transfered = true;
            
        } catch ( IOException ioe ) {
            throw new FTPSocketException( ioe );
        } finally {
            if ( out != null ) {
                try {
                    out.close();
                } catch ( IOException e ) {
                    logger.exception( e, "Closing upload: IOException" );
                }
            }
            
            if ( dch != null ) {
                logger.debug( "Closing data connection" );
                dch.close();
            }
            
            if ( transfered ) {
                validateTransfer(); // TODO check
            }
        }
        
    }        
    
    private void downloadData( InputStream is, OutputStream os ) throws IOException {
        
        boolean isAscii = FTPType.ASCII.equals( transferType );

        InputStream in = BufferFactory.newInputStream( is );
        OutputStream out = BufferFactory.newOutputStream( os );
        
        int count = in.read( buffer );
        long transfered = 0;
        
        if ( chunkSize < LINE_SEP.length ) {
            throw new IllegalArgumentException( "chunkSize < LINE_SEP.length" );
        }
        
        boolean crFound = false;
        boolean lfFound = false;
        while ( count > 0 ) {
            
            if ( isAscii ) {
                
                int startPos = 0;
                for ( int i = 0; i < count; i++ ) {
                    
                    lfFound = buffer[i] == LF;
                    
                    if ( crFound && lfFound ) {
                        if (  i -1 - startPos > 0 ) {
                            // write out from start to the cr pos
                            out.write( buffer, startPos, i -1 - startPos );
                            transfered += i -1 - startPos;
                        }
                        
                        // write out the specified line separator
                        out.write(LINE_SEP, 0, LINE_SEP.length);
                        out.flush();
                        transfered += LINE_SEP.length;
                        notifyProgressListener( transfered );
                        startPos = i +1;
                    } else if ( i == 0 && crFound ) { // cr found in the previous chunk but no lf
                        out.write( CR );
                        out.flush();
                        transfered++;
                        notifyProgressListener( transfered );
                    }
                    crFound = buffer[i] == CR;
                    
                }
                
                if ( startPos < count ) {
                    // if cr-lf boundary found, do not write cr out
                    if ( crFound ) count--;
                    out.write( buffer, startPos, count - startPos );
                    out.flush();
                    notifyProgressListener( transfered );
                }
            } else {
                out.write( buffer, 0, count );
                out.flush();
                transfered += count;
                notifyProgressListener( transfered );
            }
            
            count = in.read( buffer );
        }
        
    }
        
    // TODO abort transfer
    public void download( String remoteFile, OutputStream out ) throws FTPSocketException, InvalidReplyException {
        
//               RETR
//                  125, 150
//                     (110)
//                     226, 250
//                     425, 426, 451
        
        DataChannel dch = null;
        InputStream in = null;
        type( this.getTransferType());
        
        boolean transfered = false;
        try {
            
            dch = control.createDataConnection( this.mode );            
            
            control.sendAndValidateCommand( "RETR " + remoteFile, new int[]{ 125, 150 } );
            
            in = dch.getInputStream();
            
            downloadData( in, out );
            
            transfered = true;
            
        } catch (IOException ex) {
            throw new FTPSocketException( ex );
        } finally {
            
            if ( in != null ) {
                try {
                    in.close();
                } catch (IOException ex) {
                    logger.exception( ex, "Closing inputstream of datachannel: IOException" );
                }
            }
            if ( dch != null ) {
                dch.close();
            }
            
            if ( transfered ) {
                validateTransfer();
            }
        }
    }
    
    public Vector list() throws FTPSocketException, InvalidReplyException {
        return listCommand( "LIST" );
    }
    
    public Vector listNames() throws FTPSocketException, InvalidReplyException {
        return listCommand( "NLST" );
    }
    
    protected Vector listCommand( String command ) throws FTPSocketException, InvalidReplyException {
        
        DataChannel dch = control.createDataConnection( mode );
        
        FTPReply rp = control.sendAndValidateCommand( command, new int[]{125, 150, 450, 550} );
        
        logger.debug( "List names reply: " + rp );
        
        Vector lst = new Vector();
        
//         450 Requested file action not taken.
//             File unavailable (e.g., file busy).
//         550 Requested action not taken.
//             File unavailable (e.g., file not found, no access).
//         451 Requested action aborted. Local error in processing.
        if ( rp.getCode() != 450 && rp.getCode() != 550 ) {
            
            // founds files
            ReadBuffer rs = BufferFactory.createDefaultReadBuffer( dch.getInputStream() );
            try {
                String line = rs.readLine();
                while ( line != null ) {
                    logger.debug("List names line: " + line );
                    lst.addElement( line );
                    line = rs.readLine();
                }
                
            } catch (IOException ex) {
                throw new FTPSocketException( ex );
            } finally {
                if ( dch != null ) {
                    dch.close();
                }
            }
            
            FTPReply crp = control.readReply();
            
            // transfer compleete
            // 250 Requested file action okay, completed.
            // 226 Closing data connection.
            //     Requested file action successful (for example, file
            //     transfer or file abort).
            crp.validate( new int[]{ 226, 250 } );
            
        }
        
        return lst;
    }
    
    private void validateTransfer() throws InvalidReplyException, FTPSocketException {
        logger.debug( "Transfer validating..." );
        FTPReply rp = control.readReply();
        logger.debug( "Transfer reply: " + rp );
        rp.validate( new int[]{ 225, 226, 250, 426, 450 } );
    }
    
    private void notifyProgressListener( long transfered ) {
        for ( int i = 0; i < progressListeners.size(); i++ ) {
            ProgressListener p = (ProgressListener) progressListeners.elementAt(i);
            p.transfered( transfered );
        }
    }
    
    public int getMode() {
        return mode;
    }
    
    public void setMode(int val) {
        this.mode = val;
    }
    
    public int getRemotePort() {
        return control.getRemotePort();
    }
    
    public String getRemoteHost() {
        return control.getRemoteHost();
    }
    
    public void setRemoteHost(String host, int port) {
        control.setRemoteHost( host );
        control.setRemotePort( port );
        control.init();
    }
    
    public void addMessageListener(FTPMessageListener listener) {
        control.addMessageListener( listener );
    }
    
    public boolean remMessageListener(FTPMessageListener listener) {
        return control.remMessageListener( listener );
    }
    
    public void addProgressListener(ProgressListener listener) {
        if ( ! progressListeners.contains( listener ) ) {
            progressListeners.addElement( listener );
        }
    }
    
    public boolean remProgressListener(ProgressListener listener) {
        return progressListeners.removeElement( listener );
    }
    
    public String getTransferType() {
        return transferType;
    }
    
    public void setTransferType(String transferType) {
        this.transferType = transferType;
    }
    
    public int getChunkSize() {
        return chunkSize;
    }
    
    public void setChunkSize(int chunkSize) {
        if ( chunkSize < LINE_SEP.length ) {
            throw new IllegalArgumentException( "chunkSize < LINE_SEP.length" );
        }
        buffer = new byte[ chunkSize ];
        this.chunkSize = chunkSize;
    }
    
    public byte[] getLINE_SEP() {
        return LINE_SEP;
    }
    
    public void setLINE_SEP(byte[] LINE_SEP) {
        this.LINE_SEP = LINE_SEP;
    }
    
    public void setPortRange( int begin, int end ) {
        if ( begin > end ) {
            throw new IllegalArgumentException( "Invalid port range: begin>end" );
        }
        control.setPortRange( begin, end );
        
    }
    
    public int[] getPortRange() {
        return control.getPortRange();
    }

    public int[] getBindHost() {
        return control.getBindIp();
    }

    public void setBindHost( int[] ip ) {
        control.setBindIp( ip );
    }
}

